package ru.chaykin.load.parse;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ru.chaykin.exceptions.InvalidFileFormatException;
import ru.chaykin.load.ICachePathConsts;
import ru.chaykin.primitives.Circle;
import ru.chaykin.primitives.Point;
import ru.chaykin.primitives.Primitives;
import ru.chaykin.quadtree.Quad;
import ru.chaykin.quadtree.QuadElement;
import ru.chaykin.quadtree.QuadTreeBuilder;
import ru.chaykin.rendering.CullingDataThread;
import ru.chaykin.rendering.IRenderedAreaConsts;
import ru.chaykin.rendering.OpenGLSceneRenderer;
import ru.chaykin.viewobject.ViewObject;

public final class MIFParser implements IRegExpsPatternsConsts, ICachePathConsts, IRenderedAreaConsts, IMappedIndexBuffConsts {
	
	private static List<String> columnNames;
	private static Pattern B_PEN_PATT;
	private static Pattern B_FONT_PATT;
	
	private static final int IN_BUFF_SIZE = Float.SIZE * 32 * 1024;	//Буффер размером 1 Мб
	private static final long MAPPED_FILE_BUFF_SIZE = 64 * 1024 * 1024;	//64 мб
	private static final long MAPPED_FILE_TEXT_SIZE = 8 * 1024 * 1024;	//8 мб
	private static final long MAPPED_INDEX_FILE_TEXT_SIZE = 8 *1024 * 1024; //8 мб
	
	private static String sep = ",";	//Разделитель в файле MIF
	
	private BufferedReader in;
	private ByteBuffer dataBuff = ByteBuffer.allocate(Float.SIZE * 1024);
	private ByteBuffer indexBuff = ByteBuffer.allocate(IND_GEO_ROW_SIZE);	
	
	private ByteBuffer textBuff = ByteBuffer.allocate(Float.SIZE * 1024);
	private ByteBuffer indexTextBuff = ByteBuffer.allocate(IND_TEXT_ROW_SIZE);
	private long totalOffset = 0;
	private long totalTextOffset = 0; 
	private static long totalObjects = 0; //Общее количество найденных объектов. Нужно для отображения инфы из MID-файла
	
	public static volatile MappedByteBuffer mappedDataByteBuff; 
	public static volatile MappedByteBuffer mappedIndexByteBuff;
	public static volatile MappedByteBuffer mappedTextBuff;
	public static volatile MappedByteBuffer mappedIndexTextBuff;
	
	private static QuadTreeBuilder tree;
	
	public MIFParser(String fName) throws InvalidFileFormatException, IOException {
		openFile(fName);
		totalObjects = 0;
		B_PEN_PATT = Pattern.compile("\\s*Pen\\s+\\((\\d+)" + sep + "\\s*(\\d+)" + sep + "\\s*(\\d+)\\s*\\)\\s*", Pattern.CASE_INSENSITIVE);
		B_FONT_PATT = Pattern.compile("\\s*Font\\s+\\(\"(.*)\"\\s*" + sep + "\\s*(\\d+)\\s*" + sep + "\\s*(\\d+)\\s*" + sep + "\\s*(\\d+).*" + 
																	  "\\)\\s*", Pattern.CASE_INSENSITIVE);
		File dataFile = new File(CACHE_PATH + CACHE_FILENAME);
		RandomAccessFile rf = new RandomAccessFile(dataFile, "rw");
		FileChannel fc=rf.getChannel();
		mappedDataByteBuff = fc.map(FileChannel.MapMode.READ_WRITE, 0, MAPPED_FILE_BUFF_SIZE);
		File indexFile = new File(CACHE_PATH + CACHE_INDEX_FILENAME);
		RandomAccessFile indexrf = new RandomAccessFile(indexFile, "rw");
		FileChannel indexfc=indexrf.getChannel();
		mappedIndexByteBuff = indexfc.map(FileChannel.MapMode.READ_WRITE, 0, MAPPED_FILE_BUFF_SIZE);
		File indexTextFile = new File(CACHE_PATH + CACHE_INDEX_TEXT_FILENAME);
		RandomAccessFile indexTextrf = new RandomAccessFile(indexTextFile, "rw");
		FileChannel indexTextfc = indexTextrf.getChannel();
		mappedIndexTextBuff = indexTextfc.map(FileChannel.MapMode.READ_WRITE, 0, MAPPED_INDEX_FILE_TEXT_SIZE);
		File textFile = new File(CACHE_PATH + CACHE_TEXT_FILENAME);
		RandomAccessFile textrf = new RandomAccessFile(textFile, "rw");
		FileChannel textfc=textrf.getChannel();
		mappedTextBuff = textfc.map(FileChannel.MapMode.READ_WRITE, 0, MAPPED_FILE_TEXT_SIZE);
	}
	
	private final float[] findPen() throws IOException {
		Matcher matcher;
		try {
			matcher = B_PEN_PATT.matcher(in.readLine());
		} catch (IOException e) {
			in.close();
			throw new IOException();
		}
		float[] pen = new float[3];
		if (matcher.matches()) { 
			pen[0] = Float.parseFloat(matcher.group(1));
			pen[1] = Integer.parseInt(matcher.group(2));
			pen[2] = Integer.parseInt(matcher.group(3));
		}
		return pen;
	}
	
	public boolean findLine(Matcher matcher) throws IOException {
		if (matcher.matches()) {
			totalObjects++;
			float line_x0 = Float.parseFloat(matcher.group(1)); 
			float line_y0 = Float.parseFloat(matcher.group(3)); 
			float line_x1 = Float.parseFloat(matcher.group(5)); 
			float line_y1 = Float.parseFloat(matcher.group(7));
			Circle circle = Primitives.getCircle(line_x0, line_y0, line_x1, line_y1);
			float[] pen;
			try {
				pen = findPen();
				float[] pnts = Primitives.getLineBuffer(line_x0, line_y0, line_x1, line_y1);
				writeGeo(circle, pen, pnts);
			} catch (IOException e) {
				in.close();
				throw new IOException();
			}
			return true;
		}
		return false;
	}

	public boolean findPline(Matcher matcher) throws IOException {
		if (matcher.matches()) {
			totalObjects++;
			int pLineCount = Integer.parseInt(matcher.group(1));
			List<Point> pLine = new ArrayList<Point>();
			try {
				for (int i = 0; i < pLineCount; i++) {
					matcher = B_FLOAT_POINT_PATT.matcher(in.readLine());
					if (matcher.matches()) {
						pLine.add(new Point(Float.parseFloat(matcher.group(1)), Float.parseFloat(matcher.group(3))));
					}
					else {
						pLine.clear();
						break;
					}
				}
				if (pLine.size() > 0) {
					Circle circle = Primitives.getCircle(pLine);
					float[] pen = findPen();
					float[] pnts = Primitives.getBuffer(pLine);
					writeGeo(circle, pen, pnts);
					return true;
				}
			} catch (IOException e) {
				in.close();
				throw new IOException();
			}
			return true; //В БД не добавили, но сам B_PLINE_PATT сработал
		}
		return false;
	}
	
	public boolean findRegion(Matcher matcher) throws IOException {
		if (matcher.matches()) {
			totalObjects++;
			int regionCount = Integer.parseInt(matcher.group(1));
			try {
				for (int i = 0; i < regionCount; i++) {	//Перебираем несколько полигонов
					List<Point> region = new ArrayList<Point>();
					matcher = B_PNTS_IN_REGION_PATT.matcher(in.readLine());
					if (matcher.matches()) {
						int pntsCount = Integer.parseInt(matcher.group(1));
						for (int j = 0; j < pntsCount; j++) { //Перебираем все точки текущего полигона
							matcher = B_FLOAT_POINT_PATT.matcher(in.readLine());
							if (matcher.matches()) {
								region.add(new Point(Float.parseFloat(matcher.group(1)), Float.parseFloat(matcher.group(3))));
							}
							else {
								region.clear();
								break;
							}
						}
						if (region.size() > 0) {
							Circle circle = Primitives.getCircle(region);
							float[] pen = findPen();
							float[] pnts = Primitives.getBuffer(region);
							writeGeo(circle, pen, pnts);
						}
					}
					else
						break;
				}
			} catch (IOException e) {
				in.close();
				throw new IOException();
			}
			return true;
		}
		return false;
	}
	
	public boolean findText(Matcher matcher) throws IOException {
		if (matcher.matches()) {
			totalObjects++;
			try {
				matcher = B_TEXT_STR_PATT.matcher(in.readLine());
				if (matcher.matches()) {
					String str = matcher.group(1);
					matcher = B_TEXT_POINT_PATT.matcher(in.readLine());
					if (matcher.matches()) {
						float line_x0 = Float.parseFloat(matcher.group(1)); 
						float line_y0 = Float.parseFloat(matcher.group(3)); 
						float line_x1 = Float.parseFloat(matcher.group(5)); 
						float line_y1 = Float.parseFloat(matcher.group(7));
						matcher = B_FONT_PATT.matcher(in.readLine());
						if (matcher.matches()) {
							float color = Float.parseFloat(matcher.group(3));
							matcher = B_TEXT_ANGLE_PATT.matcher(in.readLine());
							float angle = 0;
							if (matcher.matches())
								angle = Float.parseFloat(matcher.group(1));
							writeText(line_x0, line_y0, line_x1, line_y1, color, angle, str);
						}
					}
					
					return true;
				}
			} catch (IOException e) {
				in.close();
				throw new IOException();
			}
			
		}
		return false;
	}
	
	public boolean findOther(Matcher matcher) throws IOException {
		if (matcher.matches()) { 
			totalObjects++;
			return true;
		}
		return false;
	}
	
	private void writeText(float line_x0, float line_y0, float line_x1, float line_y1, float color, float angle, String str) {
		FloatBuffer fb = textBuff.asFloatBuffer();
		fb.put(line_x0);
		fb.put(line_y0);
		fb.put(line_x1);
		fb.put(line_y1);
		fb.put(color);
		fb.put(angle);
		fb.put(totalObjects);

		textBuff.position(TEXT_CONST_PART_ROW_SIZE);
		textBuff.put(str.getBytes());
		
		Circle circle = Primitives.getCircle(line_x0, line_y0, line_x1, line_y1);
		float[] index = new float[]{circle.get_x(), circle.get_y(), circle.get_r(), 
																			totalTextOffset, TEXT_CONST_PART_ROW_SIZE + str.getBytes().length};
		indexTextBuff.asFloatBuffer().put(index);
		synchronized(mappedTextBuff) {
			mappedTextBuff.put(textBuff.array(), 0, TEXT_CONST_PART_ROW_SIZE + str.getBytes().length);
		}
		synchronized(mappedIndexTextBuff) {
			mappedIndexTextBuff.put(indexTextBuff.array(), 0, IND_TEXT_ROW_SIZE);
		}
		textBuff.clear();
		indexTextBuff.clear();
		totalTextOffset += (TEXT_CONST_PART_ROW_SIZE + str.getBytes().length);
	}

	private final void writeGeo(Circle circle, float[] pen, float[] pnts) throws IOException {
		dataBuff.asFloatBuffer().put(pnts);
		
		//Стиль вывода линии не используется, поэтому запишем сюда индекс, по которому можем найти инфу в MID-файле
		pen[1] = totalObjects;
		
		float[] index = new float[]{circle.get_x(), circle.get_y(), circle.get_r(), pen[0], pen[1], pen[2], totalOffset, pnts.length * Float.SIZE / 8};
		indexBuff.asFloatBuffer().put(index);
		synchronized(mappedDataByteBuff) {
			mappedDataByteBuff.put(dataBuff.array(), 0, pnts.length * Float.SIZE / 8);
		}
		synchronized(mappedIndexByteBuff) {
			mappedIndexByteBuff.put(indexBuff.array(), 0, 8 * Float.SIZE / 8);
		}
		tree.addObject(index);
		indexBuff.clear();
		dataBuff.clear();
		totalOffset += (pnts.length * Float.SIZE / 8);
	}
	
	private final void openFile(String fName) throws InvalidFileFormatException, IOException {
    	if (fName !=null && !fName.isEmpty()) {
			try {
				Reader reader = new InputStreamReader(new FileInputStream(fName), "CP1251");
				in = new BufferedReader(reader, IN_BUFF_SIZE);
				ViewObject.setMIDFileName(getMIDFileName(fName));
			} catch (IOException e) {
				in.close();
				throw new IOException();
			}
			try {
				CheckFileHeader();
			} catch (InvalidFileFormatException e) {
				in.close();
				throw new InvalidFileFormatException();
			} catch (IOException e) {
				in.close();
				throw new IOException();
			}
    	}
    }  
	
	/**
     * Метод проверяет формат файла. Если заголовок не соответствует формату, генерируется исключение <code>InvalidFileFormatException</code>
     * @throws InvalidFileFormatException
     * @throws IOException
     */
    private final void CheckFileHeader() throws InvalidFileFormatException, IOException {
    	String str;
    	boolean verifiedOk = false;
    	Matcher matcher;
		try {
			matcher = H_VERSION_PATT.matcher(in.readLine());
	    	if (matcher.matches()) {
	    		for (int i=0; i<6; i++) {
	    			str = in.readLine();
	    			matcher = H_DELIMETER_PATT.matcher(str);
	    			if (matcher.matches())
	    				sep = matcher.group(1);
	    			else {
		    			matcher = H_COORDSYS_PATT.matcher(str);
		    			if (matcher.matches()) {
		    				float xMin = Float.parseFloat(matcher.group(1)); 
		    				float yMin = Float.parseFloat(matcher.group(3)); 
		    				float xMax = Float.parseFloat(matcher.group(5)); 
		    				float yMax = Float.parseFloat(matcher.group(7));
		    				CullingDataThread.updateRenderedArea((xMin + xMax) /2f, (yMin + yMax) /2f, Point.distance(xMin, yMin, xMax, yMax) / 2f);
		    				tree = new QuadTreeBuilder(new Quad(xMin, xMax, yMax, yMin));
		    				OpenGLSceneRenderer.initRenderedArea();
		    			}
		    			else {
			    			matcher = H_COLUMNS_PATT.matcher(str);
			    			if (matcher.matches()) {
			    				int columns = Integer.parseInt(matcher.group(1));
			    				columnNames = new ArrayList<String>();
			    				for (int j = 0; j < columns; j++) {
			    					matcher = H_COLUMN_NAME_PATT.matcher(in.readLine());
			    					if (matcher.matches())
			    						columnNames.add(matcher.group(1));
			    					else {
			    						in.close();
			    						throw new InvalidFileFormatException();
			    					}
								}
			    				verifiedOk=true;
			    				break;
			    			}
		    			}
	    			}
	    		}
	    		if (!verifiedOk) {
	    			in.close();
	    			throw new InvalidFileFormatException();
	    		}
	    	}
	    	else {
	    		in.close();
	    		throw new InvalidFileFormatException();
	    	}
		} catch (IOException e) {
			in.close();
			throw new IOException();
		}
    }

	public BufferedReader getFileReader() {
		return in;
	}
    
	public long getObjectsCount() {
		return totalObjects;
	}
	
	public static List<String> getColumnNames() {
		return columnNames;
	}
	
	public static String getSeparator() {
		return sep;
	}
	
	public static QuadElement getQuadElement() {
		if (tree == null)
			return null;
		return tree.getRootQuad();
	}
	
	private String getMIDFileName(String mifFileName){
		int sepPos = mifFileName.lastIndexOf(File.separator);
		int extPos = mifFileName.lastIndexOf(".");
		if (sepPos > extPos) {
			return mifFileName + ".MID";
		}
		else {
			char[] chars = mifFileName.toCharArray();
			chars[chars.length - 1] = Character.isLowerCase(chars[chars.length - 1]) ? 'd' : 'D';
			return new String(chars);
		}
	}
}